package vulns

import (
	"fmt"
	"os"
	"slices"
	"sort"
	"strings"

	"github.com/google/osv-scanner/internal/semantic"
	"github.com/google/osv-scanner/pkg/lockfile"
	"github.com/google/osv-scanner/pkg/models"
)

func eventVersion(e models.Event) string {
	if e.Introduced != "" {
		return e.Introduced
	}

	if e.Fixed != "" {
		return e.Fixed
	}

	if e.Limit != "" {
		return e.Limit
	}

	if e.LastAffected != "" {
		return e.LastAffected
	}

	return ""
}

// convertEcosystem handles converting from a "lockfile" ecosystem to a "models" ecosystem.
//
// todo: this should go away in v2 once we've moved to a single ecosystem type
func convertLockfileEcosystem(version lockfile.Ecosystem) models.Ecosystem {
	b, _, _ := strings.Cut(string(version), ":")

	return models.Ecosystem(b)
}

func rangeContainsVersion(ar models.Range, pkg lockfile.PackageDetails) bool {
	if ar.Type != models.RangeEcosystem && ar.Type != models.RangeSemVer {
		return false
	}
	// todo: we should probably warn here
	if len(ar.Events) == 0 {
		return false
	}

	vp := semantic.MustParse(pkg.Version, convertLockfileEcosystem(pkg.CompareAs))

	sort.Slice(ar.Events, func(i, j int) bool {
		a := ar.Events[i]
		b := ar.Events[j]

		if a.Introduced == "0" {
			return true
		}

		if b.Introduced == "0" {
			return false
		}

		return semantic.MustParse(eventVersion(a), convertLockfileEcosystem(pkg.CompareAs)).CompareStr(eventVersion(b)) < 0
	})

	var affected bool
	for _, e := range ar.Events {
		if affected {
			if e.Fixed != "" {
				affected = vp.CompareStr(e.Fixed) < 0
			} else if e.LastAffected != "" {
				affected = e.LastAffected == pkg.Version || vp.CompareStr(e.LastAffected) <= 0
			}
		} else if e.Introduced != "" {
			affected = e.Introduced == "0" || vp.CompareStr(e.Introduced) >= 0
		}
	}

	return affected
}

// rangeAffectsVersion checks if the given version is within the range
// specified by the events of any "Ecosystem" or "Semver" type ranges
func rangeAffectsVersion(a []models.Range, pkg lockfile.PackageDetails) bool {
	for _, r := range a {
		if r.Type != models.RangeEcosystem && r.Type != models.RangeSemVer {
			return false
		}
		if rangeContainsVersion(r, pkg) {
			return true
		}
	}

	return false
}

func isAliasOfID(v models.Vulnerability, id string) bool {
	for _, alias := range v.Aliases {
		if alias == id {
			return true
		}
	}

	return false
}

func isAliasOf(v models.Vulnerability, vulnerability models.Vulnerability) bool {
	for _, alias := range vulnerability.Aliases {
		if v.ID == alias || isAliasOfID(v, alias) {
			return true
		}
	}

	return false
}

func AffectsEcosystem(v models.Vulnerability, ecosystem lockfile.Ecosystem) bool {
	for _, affected := range v.Affected {
		if areSameEcosystem(affected.Package.Ecosystem, ecosystem) {
			return true
		}
	}

	return false
}

func areSameEcosystem(a models.Ecosystem, b lockfile.Ecosystem) bool {
	majorA, minorA, foundA := strings.Cut(string(a), ":")
	majorB, minorB, foundB := strings.Cut(string(b), ":")

	// only care about the minor version if both ecosystems have one
	// otherwise we just assume that they're the same and move on
	if foundA && foundB && minorA != minorB {
		return false
	}

	return majorA == majorB
}

func IsAffected(v models.Vulnerability, pkg lockfile.PackageDetails) bool {
	for _, affected := range v.Affected {
		if areSameEcosystem(affected.Package.Ecosystem, pkg.Ecosystem) &&
			affected.Package.Name == pkg.Name {
			if len(affected.Ranges) == 0 && len(affected.Versions) == 0 {
				_, _ = fmt.Fprintf(
					os.Stderr,
					"%s does not have any ranges or versions - this is probably a mistake!\n",
					v.ID,
				)

				continue
			}

			if slices.Contains(affected.Versions, pkg.Version) {
				return true
			}

			if rangeAffectsVersion(affected.Ranges, pkg) {
				return true
			}

			// if a package does not have a version, assume it is vulnerable
			// as false positives are better than false negatives here
			if pkg.Version == "" {
				return true
			}
		}
	}

	return false
}
